package org.junyee.demo.security.ec;

import java.security.spec.ECParameterSpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;

import org.junyee.demo.security.exception.KeyException;
import org.springframework.util.Assert;

import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECFieldFp;
import java.math.BigInteger;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;

import java.security.spec.ECGenParameterSpec;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public final class EcEs {

    /**
     * KeyPairGenerator need a random seed to create key. the seed is a constant
     * value and make refresh every time.
     * 
     * @return
     */
    public KeyPair generateKeyPair() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_NAME);
            SecureRandom secureRandom = new SecureRandom();
            keyPairGenerator.initialize(eCGenParameterSpec, secureRandom);
            return keyPairGenerator.genKeyPair();
        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
            throw new KeyException(e);
        }
    }

    public Signature generateSignature() {
        try {
            return Signature.getInstance(digiestName);
        } catch (NoSuchAlgorithmException e) {
            throw new KeyException(e);
        }
    }

    public ECPrivateKey createPrivateKey(BigInteger d) {
        Assert.notNull(d, "Required D");
        ECPrivateKeySpec ecPrivateKeySpec = new ECPrivateKeySpec(d, switchEcParameterSpec());
        try {
            return (ECPrivateKey) EcUtils.KEY_FACTORY.generatePrivate(ecPrivateKeySpec);
        } catch (InvalidKeySpecException e) {
            throw new KeyException(e);
        }
    }

    public ECPublicKey createPublicKey(BigInteger x, BigInteger y) {
        Assert.notNull(x, "Required X");
        Assert.notNull(y, "Required Y");
        ECPoint ecPoint = new ECPoint(x, y);
        ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(ecPoint, switchEcParameterSpec());
        try {
            return (ECPublicKey) EcUtils.KEY_FACTORY.generatePublic(ecPublicKeySpec);
        } catch (InvalidKeySpecException e) {
            throw new KeyException(e);
        }
    }

    private ECParameterSpec switchEcParameterSpec() {
        return disableSunEcParamSpec() ? ecParameterSpec : sourceEcParameterSpec;
    }

    private void generateSourceKey() {
        if (!disableSunEcParamSpec()) {
            sourceKeyPair = generateKeyPair();
            sourceEcPrivateKey = (ECPrivateKey) sourceKeyPair.getPrivate();
            sourceEcPublicKey = (ECPublicKey) sourceKeyPair.getPublic();
            sourceEcParameterSpec = sourceEcPrivateKey.getParams();
        }
    }

    private boolean disableSunEcParamSpec() {
        return Boolean.parseBoolean(System.getProperty(SECURITY_SUN_EC_PARAM_SPEC_DISABLE, "true"));
    }

    private EcEs(String alg, String curve, String digiest, int byteLength, ECParameterSpec ecParameterSpec) {
        this.algorithm = alg;
        this.curveName = curve;
        this.digiestName = digiest;
        // removed
        this.byteLength = byteLength;
        this.ecParameterSpec = ecParameterSpec;
        this.eCGenParameterSpec = new ECGenParameterSpec(curveName);
        generateSourceKey();
    }

    public static final String SECURITY_SUN_EC_PARAM_SPEC_DISABLE = "security.sun.ec.paramspec.disable";
    private static final String EC_NAME = "EC";
    public static final EcEs ES128 = new EcEs("ES128", "secp128r1", "SHA1withECDSA", 32,
            new ECParameterSpec(
                    new EllipticCurve(new ECFieldFp(new BigInteger("340282366762482138434845932244680310783")),
                            new BigInteger("340282366762482138434845932244680310780"),
                            new BigInteger("308990863222245658030922601041482374867")),
                    new ECPoint(new BigInteger("29408993404948928992877151431649155974"),
                            new BigInteger("275621562871047521857442314737465260675")),
                    new BigInteger("340282366762482138443322565580356624661"), 1));
    public static final EcEs ES256 = new EcEs("ES256", "secp256r1", "SHA256withECDSA", 64, new ECParameterSpec(
            new EllipticCurve(
                    new ECFieldFp(new BigInteger(
                            "115792089210356248762697446949407573530086143415290314195533631308867097853951")),
                    new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
                    new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291")),
            new ECPoint(new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
                    new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109")),
            new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 1));
    public static final EcEs ES384 = new EcEs("ES384", "secp384r1", "SHA384withECDSA", 96, new ECParameterSpec(
            new EllipticCurve(new ECFieldFp(new BigInteger(
                    "39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319")),
                    new BigInteger(
                            "39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112316"),
                    new BigInteger(
                            "27580193559959705877849011840389048093056905856361568521428707301988689241309860865136260764883745107765439761230575")),
            new ECPoint(new BigInteger(
                    "26247035095799689268623156744566981891852923491109213387815615900925518854738050089022388053975719786650872476732087"),
                    new BigInteger(
                            "8325710961489029985546751289520108179287853048861315594709205902480503199884419224438643760392947333078086511627871")),
            new BigInteger(
                    "39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643"),
            1));
    public static final EcEs ES512 = new EcEs("ES512", "secp521r1", "SHA512withECDSA", 132, new ECParameterSpec(
            new EllipticCurve(new ECFieldFp(new BigInteger(
                    "6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151")),
                    new BigInteger(
                            "6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057148"),
                    new BigInteger(
                            "1093849038073734274511112390766805569936207598951683748994586394495953116150735016013708737573759623248592132296706313309438452531591012912142327488478985984")),
            new ECPoint(new BigInteger(
                    "2661740802050217063228768716723360960729859168756973147706671368418802944996427808491545080627771902352094241225065558662157113545570916814161637315895999846"),
                    new BigInteger(
                            "3757180025770020463545507224491183603594455134769762486694567779615544477440556316691234405012945539562144444537289428522585666729196580810124344277578376784")),
            new BigInteger(
                    "6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449"),
            1));

    public static EcEs getByBigInteger(BigInteger bigInteger) {
        Assert.notNull(bigInteger, "Args Required");
        byte[] cb = EcUtils.toBytesUnsigned(bigInteger);
        switch (cb.length) {
            // case 15:
            case 16: // st
            case 17:
                return ES128;
            // case 31:
            case 32: // st
            case 33:
            case 34:
                return ES256;
            // case 47:
            case 48: // st
            case 49:
                return ES384;
            // case 64:
            case 65: // st
            case 66:
                return ES512;
            default:
                throw new KeyException("Not found EC ES [field Size] " + cb.length);

        }
    }

    private String algorithm;
    private String curveName;
    private String digiestName;
    private int byteLength;
    private ECParameterSpec ecParameterSpec;
    private ECGenParameterSpec eCGenParameterSpec;
    private KeyPair sourceKeyPair;
    private ECParameterSpec sourceEcParameterSpec;
    private ECPrivateKey sourceEcPrivateKey;
    private ECPublicKey sourceEcPublicKey;
}
